iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0

上一篇,我們用最快的方式,透過shader製作了球體。基本上這個球體就跟MeshBasicMaterial({color: 0x4c99ff})一樣,其球體範圍內的像素全是藍色。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505K52Horc4jD.png

本篇將介紹Shader的概念。我們將透過快速修改Shader,先做出一個成果,然後解釋其原理。

我將介紹以下內容:

  1. Shader是什麼?
    1. Shader是怎麼運作的呢?
    2. Three.js之於Shader
    3. 要寫shader,就必須理解它所用的程式語言:GLSL
  2. 這到底是什麼原理?從認識GLSL開始
  3. GLSL必須知道的變數與函式

Shader是什麼?

Shader是怎麼運作的呢?

基本上是這樣的:three.js的程式碼事實上也是在shader運行,shader是各式各樣3D渲染函式庫(使用WebGLRenderer的函式庫)的底層,它渲染所有像素。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505PqyVuNnIkO.png

所以說:當你在使用three.js、P5.js、babylon.js或WebGL API時,你就正在用Shader,只是它在底層。

Three.js之於Shader

如果你還有印象的話,你應該會記得在「Day8: Three.js 你有被光速踢過嗎?解析3D界的黃猿——光的底層原理與介紹」討論到:three.js有光、有球體等物件。光照射到球體,出現亮面跟暗面。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505iF7msjZ6ou.png

為什麼可以產生亮面跟暗面?這是因為,three.js也正在用shader,每當它執行一個像素,就會計算該像素其光的強度。

光的強度 = 法線的單位向量.向光的單位向量
        = |法線|*|向光|*cos(θ)

一旦像素計算結果趨近1,則該像素最亮,這是因為亮面的向光向量跟像素所處的球面法線向量相近;相反的,當像素計算結果趨近0,代表兩向量角度接近直角。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505E8p4zrHLqQ.png

也就是說,three.js之所以可以幫每一個像素創造亮面跟暗面,那是因為它能夠在shader運算上述的邏輯。都是shader的功勞。

要寫shader,就必須理解它所用的程式語言:GLSL

我們看完了three.js的底層是用shader創造而成,那我們其實也可以實作光源。我們將在下一篇創造環境光,但在這之前,我們必須讀懂shader所使用的程式語言GLSL。

寫Shader從認識GLSL開始

寫Shader從認識GLSL開始:專門為GPU設計的程式語言

GLSL其實跟C語言很相近,如果你過去熟悉C語言,那你應該很快就能上手。然而如果沒有接觸C語言就來涉獵Shader的話,你可能會需要知道如何理解GLSL,本篇介紹GLSL中所需理解的先備知識。

GLSL其實跟C語言很相近,但如果沒有接觸過C語言就跑來前端的話,那可能需要惡補一下,以下介紹GLSL:

寫Shader從認識GLSL開始:變數宣告

GLSL的變數相當多種,舉凡int, float, bool, vec2, vec3, vec4等,一一介紹:

  • int, float, bool

    前三個你應該可以猜的出來,就是整數、浮點數、布林值

     // 請務必加上分號
    int color = 1;
    float brighness = 0.0;
    bool isWhite = true;
    
    
  • vec2, vec3, vec4

    1. 它們即是Vector的縮寫,它可以一個數組,可以是二維、三維、四維,依照你的需求而定。

      vec2 position = vec2(1.0, -1.0);
      vec3 color = vec3(1.0, 0.0, 0.5);
      vec4 colorWithAlpha = vec4(1.0, 0.0, 0.5, 0.5);
      
      

      vec2可以裝兩個數值,vec3可以裝三個數值,以此類推

      如果想像成物件存取這些可能比較好理解。

      // 類似js的
      const color = {r: 0, g:0, b:0}
      
      

      跟js的差別在於:

      • GLSL對型別是嚴格的,浮點數跟整數不得輕易轉換,但javascript可互通。
      • GLSL的key並不是r,g,b三種取值,詳情請繼續往下看。
    2. 數組的型別不限制僅為浮點數,整數、布林值都可以。但強烈建議都用浮點數。

      vec3 redColor = vec3(1, 0, 0); // 給定整數合法,將得出 vec3(1.0,0.0,0.0)
      vec3 yellowColor = vec3(1, true, false); // 給定布林合法,將得出 vec3(1.0,1.0,0.0)
      vec3 whiteColor = vec3(1, true, 1.0); // 多重型別合法,將得出 vec3(1.0,1.0,1.0)
      
      
    3. 存取數組中的值,有多種別名。存取時,我們可以分別用r,g,b來依序取得數值,也可以x,y,z依序取得數值。

      vec4 redColor = vec4(0.8, 0.3, 0.2, 1.0);
      // 以下都能取得數組第一個值
      float red = redColor.r // 0.8
      float x = redColor.x // 0.8
      
      // 以下都能取得數組第二個值
      float red = redColor.g // 0.3
      float x = redColor.y // 0.3
      
      // 以下都能取得數組第三個值
      float red = redColor.b // 0.2
      float x = redColor.z // 0.2
      
      // 以下都能取得數組第四個值
      float red = redColor.a // 1.0
      float x = redColor.w // 1.0
      
      
    4. 取得多重數值的方式。GLSL中,可以取出多個數值,其解構的方式十分便利。

      vec3 redColor = vec3(0.8, 0.3, 0.2);
      // 可以指定成
      vec4 noColor = vec3(redColor ,1.0) // 等於vec3(0.8, 0.3, 0.2. 1.0);
      // 也可寫作
      vec4 noColor = vec3(redColor.xyz ,1.0) // 等於vec3(0.8, 0.3, 0.2. 1.0);
      // 可以抽特定的數值
      vec4 noColor = vec3(redColor.xy ,0.7 ,1.0) // 等於vec3(0.8, 0.3, 0.7. 1.0);
      // 抽取順序可以調整
      vec4 noColor = vec3(redColor.yz ,0.7 ,1.0) // 等於vec3(0.3, 0.8, 0.7. 1.0);
      
      

寫Shader從認識GLSL開始:函式宣告

函式跟JS的函式很像,但還是有差異,估計你可以直接透過用看的就能看出兩者差異:

// 回傳型別、函式名稱、參數
vec3 rgb(float r, float g, float b){
  return vec3(r / 255.0, g / 255.0, b / 255.0);
}

寫Shader從認識GLSL開始:程式進入點

我們有兩個Shader,vertextShader每個錨點執行一次,fragmentShader每一個像素執行一次。

凡是執行,就得有程式進入點。因為這不是逐行執行的腳本語言,這是程式語言,要編譯的那種。

而這也是為什麼,我們的fragmentShader跟vertextShader有main(),因為它就是程式開始執行的地方。

// vertex shader
void main(void){
	gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}

// fragment shader
void main(void){
  gl_FragColor=vec4(0.0, 0.0, 0.0, 1.);
}

寫Shader從認識GLSL開始:程式目標

Shader有它的任務。vertex shader需要取得渲染範圍,而fragment shader需要每顆像素的顏色。

對vertex shader來說,只要得到gl_Position數值(錨點最終在螢幕上的位置),就算完成任務。對fragment shader來說,只要得到gl_FragColor(像素的顏色),就算完成任務。任務結束之後,剩下的邏輯都沒有必要了。

  • 以vertex shader來說:

    // vertex shader
    // gl_Position代表螢幕像素的位置
    gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
    
    
    • gl_Position到底是什麼意義?

      由於vertext shader每一個錨點執行一次,所以它可以取得球體的所有頂點,這也使得它能找出球體的形狀,在我們的電腦螢幕中,裁剪出一個範圍。

      而這個範圍,使得fragment shader在運算每一個像素的顏色時,能夠得知有哪些像素需要運算,哪些不用運算。

      以我們的例子來說,球體錨點中的範圍需要運算,球體以外的不需要運算。

      https://ithelp.ithome.com.tw/upload/images/20221015/20142505NKcWgKnpkw.png

    • projectionMatrixmodelViewMatrix 是什麼意思?

      主要是負責將錨點從世界空間中,基於鏡頭的位置,投影到螢幕這平面的座標中。這個之後可以介紹。

  • 以fragment shader來說:

    // fragment shader
    // gl_FragColor代表每一個像素的顏色
    gl_FragColor=vec4(0.0, 0.0, 0.0, 1.0); // 黑色
    
    

    gl_FragColor就是每一個像素的顏色。

    如果有100個像素,不就要設定100個gl_FragColor嗎?這倒不用。設定1個就可以了,因為fragment shader每一顆像素都會各自執行fragment shader,每一顆像素都只要有一組RGBA顏色,所以一個gl_FragColor就可以了。

小結

我們快速修改Shader,並且介紹GLSL。然而還沒完呢!下一篇將繼續介紹GLSL,同時提供小東西實作。


上一篇
Day22: WebGL Shader—你好啊大哥哥,沒想到你可以到Shader來呢!
下一篇
Day24: WebGL Shader——透過自製環境光實作shader傳值
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言